package com.github.fork.json;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.*;
import org.springframework.util.StringUtils;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;

/**
 * @author Jerry Liu
 * @since 2017/3/22
 */
@Provider
public class FastJsonProvider implements
        MessageBodyReader<Object>, MessageBodyWriter<Object> {

    private static final String json = "json";
    private static final String pjson = "+json";
    private static final String js = "javascript";
    private static final String xjs = "x-javascript";

    private boolean annotated = false;
    private String[] scanPackages = null;
    private Class<?>[] clazzes = null;
    private boolean pretty;

    private FastJsonConfig jsonConfig = new FastJsonConfig(new SerializeConfig(), null, null, new ParserConfig(), null);

    public FastJsonProvider() {
    }

    public FastJsonProvider(boolean annotated) {
        this.annotated = annotated;
    }

    public FastJsonProvider(String[] scanPackages) {
        this.scanPackages = scanPackages;
    }

    public FastJsonProvider(boolean annotated, String[] scanPackages) {
        this.annotated = annotated;
        this.scanPackages = scanPackages;
    }

    public FastJsonProvider(Class<?>[] clazzes) {
        this.clazzes = clazzes;
    }

    /**
     * Initiate FastJsonConfig
     * @param jsonConfig {@link FastJsonConfig}
     * @return FastJsonProvider instance
     */
    public FastJsonProvider init(FastJsonConfig jsonConfig) {
        this.jsonConfig = jsonConfig;
        return this;
    }

    /**
     * Set pretty format
     * @param pretty true/false
     * @return FastJsonProvider instance
     */
    public FastJsonProvider setPretty(boolean pretty) {
        this.pretty = pretty;
        return this;
    }

    @Override
    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return hasMatchingMediaType(mediaType) && isValidType(type);
    }

    @Override
    public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
                           MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
            throws IOException, WebApplicationException {
        String input = null;

        try {
            input = IOUtils.read(entityStream);
        } catch (Exception e) {

        }
        if (input == null)
            return null;

        if (jsonConfig.getFeatures() == null) {
            return JSON.parseObject(input, type, jsonConfig.getParserConfig(), JSON.DEFAULT_PARSER_FEATURE);
        }

        return JSON.parseObject(input, type, jsonConfig.getParserConfig(),
                JSON.DEFAULT_PARSER_FEATURE, jsonConfig.getFeatures());
    }

    @Override
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return hasMatchingMediaType(mediaType) && isValidType(type);
    }

    @Override
    public long getSize(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return -1;
    }

    @Override
    public void writeTo(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
                        MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {

        SerializeFilter filter = null;
        if (pretty) {
            if (jsonConfig.getSerializerFeatures() == null) {
                jsonConfig.setSerializerFeatures(new SerializerFeature[]{SerializerFeature.PrettyFormat});
            } else {
                List<SerializerFeature> serializerFeatures = Arrays.asList(jsonConfig.getSerializerFeatures());
                serializerFeatures.add(SerializerFeature.PrettyFormat);
                jsonConfig.setSerializerFeatures(serializerFeatures.toArray(new SerializerFeature[]{}));
            }
        }

        if (jsonConfig.getSerializeFilters() != null) {
            filter = jsonConfig.getSerializeFilters().get(type);
        }

        String jsonStr = toJSONString(o, filter, jsonConfig.getSerializerFeatures());
        if (!StringUtils.isEmpty(jsonStr)) {
            entityStream.write(jsonStr.getBytes());
        }
    }

    private boolean isValidType(Class<?> type) {

        if (type == null) {
            return false;
        }

        if (annotated) {
            return checkAnnotation(type);
        }

        if (scanPackages != null) {
            String classPackage = type.getPackage().getName();
            for (String pkg : scanPackages) {
                if (classPackage.startsWith(pkg)) {
                    return !annotated || checkAnnotation(type);
                }
            }
            return false;
        }

        if (clazzes != null) {
            for (Class<?> clazz : clazzes) {
                if (clazz == type) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

    private boolean checkAnnotation(Class<?> type) {
        Annotation[] annotations = type.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation instanceof FastJsonType) {
                return true;
            }
        }
        return false;
    }

    private boolean hasMatchingMediaType(MediaType mediaType) {
        if (mediaType != null) {
            String subtype = mediaType.getSubtype();
            return json.equalsIgnoreCase(subtype)
                    || subtype.endsWith(pjson)
                    || js.equals(subtype)
                    || xjs.equals(subtype);
        }
        return true;
    }

    private String toJSONString(Object object, SerializeFilter filter, SerializerFeature[] features) {

        SerializeWriter out = new SerializeWriter();

        try {
            JSONSerializer serializer = new JSONSerializer(out, jsonConfig.getSerializeConfig());
            if (features != null) {
                for (SerializerFeature feature : features) {
                    serializer.config(feature, true);
                }
            }

            if (filter != null) {
                if (filter instanceof PropertyFilter) {
                    serializer.getPropertyPreFilters().add((PropertyPreFilter) filter);
                }

                if (filter instanceof NameFilter) {
                    serializer.getNameFilters().add((NameFilter)filter);
                }

                if (filter instanceof ValueFilter) {
                    serializer.getValueFilters().add((ValueFilter)filter);
                }

                if (filter instanceof PropertyFilter) {
                    serializer.getPropertyFilters().add((PropertyFilter) filter);
                }

                if (filter instanceof BeforeFilter) {
                    serializer.getBeforeFilters().add((BeforeFilter)filter);
                }

                if (filter instanceof AfterFilter) {
                    serializer.getAfterFilters().add((AfterFilter)filter);
                }
            }
            serializer.write(object);
            return out.toString();
        } finally {
            out.close();
        }
    }

}
